﻿#region Usings

using System;
using System.Collections;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;

#endregion

namespace SmartLib.UiComponents.InputControls
{
	/// <summary>
	/// </summary>
	public class AutoCompleteTextBox : TextBox
	{
		/// <summary>
		/// </summary>
		public static readonly DependencyProperty HintItemsProperty =
			DependencyProperty.Register("HintItems", typeof (IEnumerable),
			                            typeof (AutoCompleteTextBox), new UIPropertyMetadata(OnHintItemsPropertyChanged));

		/// <summary>
		/// </summary>
		public static readonly DependencyProperty HintTextProperty =
			DependencyProperty.Register("HintText", typeof (string), typeof (AutoCompleteTextBox),
			                            new FrameworkPropertyMetadata(null,
			                                                          FrameworkPropertyMetadataOptions.BindsTwoWayByDefault |
			                                                          FrameworkPropertyMetadataOptions.AffectsRender));

		/// <summary>
		/// </summary>
		public static readonly DependencyProperty IsHintsPopupOpenProperty =
			DependencyProperty.Register("IsHintsPopupOpen", typeof (bool), typeof (AutoCompleteTextBox));

		/// <summary>
		/// </summary>
		public static readonly DependencyProperty DisplayMemberPathProperty =
			DependencyProperty.Register("DisplayMemberPath", typeof (string), typeof (AutoCompleteTextBox));

		/// <summary>
		/// </summary>
		public static readonly DependencyProperty SelectedValueProperty =
			DependencyProperty.Register("SelectedValue", typeof (object), typeof (AutoCompleteTextBox));

		/// <summary>
		/// </summary>
		public static readonly DependencyProperty SelectedValuePathProperty =
			DependencyProperty.Register("SelectedValuePath", typeof (string), typeof (AutoCompleteTextBox));

		/// <summary>
		/// </summary>
		public static readonly DependencyProperty SelectedItemProperty =
			DependencyProperty.Register("SelectedItem", typeof (object), typeof (AutoCompleteTextBox));

		/// <summary>
		/// </summary>
		public static readonly DependencyProperty SelectedIndexProperty =
			DependencyProperty.Register("SelectedIndex", typeof (int), typeof (AutoCompleteTextBox), new UIPropertyMetadata(-1));

		private bool _ignoreSetText;
		private bool _ignoreTextChanges;

		/// <summary>
		/// 	Initializes the <see cref = "AutoCompleteTextBox" /> class.
		/// </summary>
		static AutoCompleteTextBox()
		{
			DefaultStyleKeyProperty.OverrideMetadata(typeof (AutoCompleteTextBox),
			                                         new FrameworkPropertyMetadata(typeof (AutoCompleteTextBox)));
		}

		/// <summary>
		/// 	Initializes a new instance of the <see cref = "AutoCompleteTextBox" /> class.
		/// </summary>
		public AutoCompleteTextBox()
		{
			HintsViewSource = new CollectionViewSource();
			HintsViewSource.Filter += HintsViewSource_Filter;
			SetCurrentValue(HintItemsProperty, new ObservableCollection<string>());
		}

		/// <summary>
		/// 	Gets or sets the hint items.
		/// </summary>
		/// <value>
		/// 	The hint items.
		/// </value>
		public IEnumerable HintItems
		{
			get { return (IEnumerable) GetValue(HintItemsProperty); }
			set { SetValue(HintItemsProperty, value); }
		}

		/// <summary>
		/// 	Gets or sets the hint text.
		/// </summary>
		/// <value>
		/// 	The hint text.
		/// </value>
		public string HintText
		{
			get { return (string) GetValue(HintTextProperty); }
			set { SetValue(HintTextProperty, value); }
		}

		/// <summary>
		/// 	Gets or sets the hints view source.
		/// </summary>
		/// <value>
		/// 	The hints view source.
		/// </value>
		public CollectionViewSource HintsViewSource { get; private set; }

		/// <summary>
		/// 	Gets or sets a value indicating whether this instance is hints popup open.
		/// </summary>
		/// <value>
		/// 	<c>true</c> if this instance is hints popup open; otherwise, <c>false</c>.
		/// </value>
		public bool IsHintsPopupOpen
		{
			get { return (bool) GetValue(IsHintsPopupOpenProperty); }
			set { SetValue(IsHintsPopupOpenProperty, value); }
		}

		/// <summary>
		/// 	Gets or sets the display member path.
		/// </summary>
		/// <value>
		/// 	The display member path.
		/// </value>
		public string DisplayMemberPath
		{
			get { return (string) GetValue(DisplayMemberPathProperty); }
			set { SetValue(DisplayMemberPathProperty, value); }
		}

		/// <summary>
		/// 	Gets or sets the selected value.
		/// </summary>
		/// <value>
		/// 	The selected value.
		/// </value>
		public object SelectedValue
		{
			get { return GetValue(SelectedValueProperty); }
			set { SetValue(SelectedValueProperty, value); }
		}

		/// <summary>
		/// 	Gets or sets the selected value path.
		/// </summary>
		/// <value>
		/// 	The selected value path.
		/// </value>
		public string SelectedValuePath
		{
			get { return (string) GetValue(SelectedValuePathProperty); }
			set { SetValue(SelectedValuePathProperty, value); }
		}

		/// <summary>
		/// 	Gets or sets the selected item.
		/// </summary>
		/// <value>
		/// 	The selected item.
		/// </value>
		public object SelectedItem
		{
			get { return GetValue(SelectedItemProperty); }
			set { SetValue(SelectedItemProperty, value); }
		}

		/// <summary>
		/// 	Gets or sets the index of the selected.
		/// </summary>
		/// <value>
		/// 	The index of the selected.
		/// </value>
		public int SelectedIndex
		{
			get { return (int) GetValue(SelectedIndexProperty); }
			set { SetValue(SelectedIndexProperty, value); }
		}

		/// <summary>
		/// 	Sets the text.
		/// </summary>
		/// <param name = "text">The text.</param>
		private void SetText(string text)
		{
			SetCurrentValue(TextProperty, text);
		}

		/// <summary>
		/// 	Sets the hint text.
		/// </summary>
		/// <param name = "hintText">The hint text.</param>
		private void SetHintText(string hintText)
		{
			SetCurrentValue(HintTextProperty, hintText);
		}

		/// <summary>
		/// 	Sets the is hints popup open.
		/// </summary>
		/// <param name = "isOpen">if set to <c>true</c> [is open].</param>
		private void SetIsHintsPopupOpen(bool isOpen)
		{
			SetCurrentValue(IsHintsPopupOpenProperty, isOpen);
		}

		/// <summary>
		/// 	Called when [hint items property changed].
		/// </summary>
		/// <param name = "d">The d.</param>
		/// <param name = "e">The <see cref = "System.Windows.DependencyPropertyChangedEventArgs" /> instance containing the event data.</param>
		private static void OnHintItemsPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
		{
			var sender = d as AutoCompleteTextBox;
			if (sender == null) return;
			sender.HintsViewSource.SortDescriptions.Clear();
			sender.HintsViewSource.Source = sender.HintItems;
			sender.HintsViewSource.SortDescriptions.Add(!string.IsNullOrEmpty(sender.DisplayMemberPath)
			                                            	? new SortDescription(sender.DisplayMemberPath,
			                                            	                      ListSortDirection.Ascending)
			                                            	: new SortDescription());
		}

		/// <summary>
		/// 	Handles the Filter event of the HintsViewSource control.
		/// </summary>
		/// <param name = "sender">The source of the event.</param>
		/// <param name = "e">The <see cref = "System.Windows.Data.FilterEventArgs" /> instance containing the event data.</param>
		private void HintsViewSource_Filter(object sender, FilterEventArgs e)
		{
			if (_ignoreTextChanges) return;
			if (e.Item == null ||
			    string.IsNullOrEmpty(Text) ||
			    string.IsNullOrWhiteSpace(Text) ||
			    string.IsNullOrEmpty(Text.Trim()))
			{
				e.Accepted = false;
				return;
			}

			var txt = Text.Trim();
			var str = GetItemValue(e.Item, DisplayMemberPath);
			e.Accepted = str != null && str.ToLowerInvariant().Contains(txt.ToLowerInvariant());
		}

		protected static string GetItemValue(object item, string propName)
		{
			if (item == null) return null;
			string str = null;
			if (!(item is string) && !string.IsNullOrEmpty(propName))
			{
				var info = item.GetType().GetProperty(propName);
				if (info != null)
				{
					var value = info.GetValue(item, Type.EmptyTypes);
					if (value != null) str = value.ToString();
				}
			}
			else
			{
				str = item.ToString();
			}
			return str;
		}

		/// <summary>
		/// 	Is called when content in this editing control changes.
		/// </summary>
		/// <param name = "e">The arguments that are associated with the <see cref = "E:System.Windows.Controls.Primitives.TextBoxBase.TextChanged" /> event.</param>
		protected override void OnTextChanged(TextChangedEventArgs e)
		{
			try
			{
				if (!IsFocused || _ignoreTextChanges || HintsViewSource.View == null) return;

				_ignoreSetText = true;
				HintsViewSource.View.Refresh();

				if (string.IsNullOrEmpty(Text) || HintsViewSource.View.IsEmpty)
				{
					SetHintText(null);
					SetIsHintsPopupOpen(false);
				}
				else if (!HintsViewSource.View.IsEmpty)
				{
					string hint = null;
					var txt = Text.Trim();
					foreach (var item in HintsViewSource.View)
					{
						var str = GetItemValue(item, DisplayMemberPath);
						if (!str.ToLowerInvariant().StartsWith(txt, StringComparison.InvariantCultureIgnoreCase)) continue;
						var postfix = (str.Length > txt.Length) ? str.Substring(txt.Length) : null;
						hint = txt + postfix;
						break;
					}
					SetHintText(hint);
					SetIsHintsPopupOpen(true);
				}
			}
			finally
			{
				base.OnTextChanged(e);
				_ignoreSetText = false;
			}
		}

		/// <summary>
		/// 	Called when the <see cref = "E:System.Windows.UIElement.KeyDown" /> occurs.
		/// </summary>
		/// <param name = "e">The event data.</param>
		protected override void OnPreviewKeyDown(KeyEventArgs e)
		{
			try
			{
				_ignoreTextChanges = true;
				switch (e.Key)
				{
					case Key.Up:
						HintsViewSource.View.MoveCurrentToPrevious();
						if (HintsViewSource.View.IsCurrentBeforeFirst)
						{
							HintsViewSource.View.MoveCurrentToNext();
							return;
						}
						break;

					case Key.Down:
						if (e.KeyboardDevice.Modifiers == ModifierKeys.Control &&
						    !HintsViewSource.View.IsEmpty)
						{
							SetIsHintsPopupOpen(true);
							return;
						}

						HintsViewSource.View.MoveCurrentToNext();
						if (HintsViewSource.View.IsCurrentAfterLast)
						{
							HintsViewSource.View.MoveCurrentToPrevious();
							return;
						}
						break;

					case Key.Tab:
					case Key.Enter:
						e.Handled = true;
						SelectCurrentText();
						Select(Text.Length, 0);
						SetIsHintsPopupOpen(false);
						break;

					case Key.Space:
						if (e.KeyboardDevice.Modifiers != ModifierKeys.Control) return;
						e.Handled = true;
						SelectCurrentText();
						Select(Text.Length, 0);
						SetIsHintsPopupOpen(false);
						break;

					case Key.Escape:
						SetIsHintsPopupOpen(false);
						break;
				}
			}
			finally
			{
				_ignoreTextChanges = false;
				base.OnPreviewKeyDown(e);
			}
		}

		/// <summary>
		/// 	Selects the current text.
		/// </summary>
		private void SelectCurrentText()
		{
			var hint = HintText ?? Text;
			if (string.IsNullOrEmpty(hint)) return;

			string starts = null;
			string equals = null;
			string contains = null;
			object selectedItem = null;

			foreach (var item in HintsViewSource.View)
			{
				var str = GetItemValue(item, DisplayMemberPath);
				if (str.Equals(hint, StringComparison.InvariantCultureIgnoreCase))
				{
					equals = str;
					selectedItem = item;
				}
				else if (str.StartsWith(hint, StringComparison.InvariantCultureIgnoreCase))
				{
					starts = str;
					selectedItem = item;
				}
				else if (str.Contains(hint))
				{
					contains = str;
					selectedItem = item;
				}
			}

			if (selectedItem != null)
			{
				SetCurrentValue(SelectedItemProperty, selectedItem);
			}

			if (!string.IsNullOrEmpty(equals))
			{
				SetHintText(equals);
				SetText(equals);
				SetIsHintsPopupOpen(false);
			}
			else if (!string.IsNullOrEmpty(starts))
			{
				SetHintText(starts);
				SetText(starts);
				SetIsHintsPopupOpen(false);
			}
			else if (!string.IsNullOrEmpty(contains))
			{
				SetHintText(contains);
				SetText(contains);
				SetIsHintsPopupOpen(false);
			}
		}

		/// <summary>
		/// 	Is called when a control template is applied.
		/// </summary>
		public override void OnApplyTemplate()
		{
			base.OnApplyTemplate();
			var lstBox = GetTemplateChild("HintsListBox") as ListBox;
			if (lstBox == null) return;
			lstBox.SelectionChanged += OnHintsListBoxSelectionChanged;
		}

		/// <summary>
		/// 	Called when [hints list box selection changed].
		/// </summary>
		/// <param name = "sender">The sender.</param>
		/// <param name = "e">The <see cref = "System.Windows.Controls.SelectionChangedEventArgs" /> instance containing the event data.</param>
		private void OnHintsListBoxSelectionChanged(object sender, SelectionChangedEventArgs e)
		{
			if (_ignoreSetText) return;
			var lstBox = sender as ListBox;
			if (lstBox == null) return;

			var hintTxt = GetItemValue(lstBox.SelectedItem, DisplayMemberPath);
			SetHintText(hintTxt);
			SetText(HintText);
			Select(Text.Length, 0);
		}
	}
}